package es.upm.gib.mappingAPI2;

import es.upm.gib.owlbasicmodel2.OWLBasicModel2;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This class is represented by a list of triples and a map of restrictions.
 * It represents a Path of the model.
 */
public class Path implements Serializable{

    //=======================================================================
    //ATTRIBUTES
    //=======================================================================

    /**
     * Vector of Triple objects composing this path. The destination class of a triple must equal the origin class
     * of the next triple in this vector
     */
    private ArrayList<Triple> path;

    /**
     * Restrictions included in this path. A restriction is an extra condition that must be fulfilled.
     */
    private Map<Integer,Restriction> restrictions = new HashMap<Integer, Restriction>();

//=======================================================================
    //CONSTRUCTORS
    //=======================================================================

    /**
     * Creates an empty Path
     */
    public Path() {
        path = new ArrayList<Triple>();
    }


    /**
     * Create a Path using the vector of triples given as parameter
     *
     * @param vec a vector of triple that represents a path
     */
    public Path(ArrayList<Triple> vec) {
        path = vec;
    }

    /**
     * Creates a Path with only one triple. The path will be a class-relation-class type path
     *
     * @param firstTriple the first triple of the Path
     */
    public Path(Triple firstTriple) {
        path = new ArrayList<Triple>();
        path.add(firstTriple);
    }

    //=======================================================================
    //PUBLIC METHODS
    //=======================================================================

    /**
     * Retrieves the list of triples of the path
     * @return triples of the path
     */
    public ArrayList<Triple> getPath() {
        return path;
    }

    /**
     * Set the triples of the path
     * @param path List of triples of the path
     */
    public void setPath(ArrayList<Triple> path) {
        this.path = path;
    }

    /**
     * Adds a new triple to the end of the path. Care must be taken to ensure that the origen of the given triple
     * equals the destination of the last triple in the path
     *
     * @param triple the new triple to add
     */
    public void add(Triple triple) {
        path.add(triple);
    }


    /**
     * Adds a path appending it at the end of the existing Path. Care must be taken to ensure that the origen of
     * the given triple
     *
     * @param pt a path to add
     */
    public void add(Path pt) {
        for (int i = 0; i < pt.size(); i++) {
            path.add(pt.get(i));
        }
    }

    /**
     * Associates the given Map as parameter with the set of restrictions of this path
     * <p/>
     * the hashmap associate the classes of the path with a restriction.
     * <p/>
     * so in the path
     * ClassA -> Rel1 -> ClassB -> Rel2 -> ClassC
     * <p/>
     * if we want to associate the restriction "res=3" to ClassB and "res2=cell" to classC
     * we have to produce an hashmap that contains this entries:
     * 1 -> res=3
     * 2 -> res2=cell
     * <p/>
     * note that for this purpose ClassB is in position 1 and ClassC is in position 2
     *
     * @param index The index of the path containing the restriction
     * @param rest The restriction to be added
     */
    public void setRestriction(Integer index, Restriction rest) {
        restrictions.put(index,rest);
    }

    /**
     * returns the restriction associated with the class number "index" in the path.
     * <p/>
     * the classes are numbered from 0 to n in the path.
     *
     * @param index the number of the class selected
     * @return the restriction of the class index
     */
    public Restriction getRestrictions(int index) {
        return restrictions.get(index);
    }

    /**
     * returns the whole set of restrictions
     *
     * @return Map containing the restrictions
     */
    public Map<Integer, Restriction> getRestrictions() {
        return restrictions;
    }

    /**
     * Returns the number of triples composing this Path
     *
     * @return the number of Triples that compose the Path
     */
    public int size() {
        return path.size();
    }

    /**
     * Return the number of classes that composes the Path
     *
     * @return the number of classes that composes the Path
     */
    public int getClassNumber() {
        return path.size() + 1;
    }


    /**
     * Returns the Triple in a given position (starting at 0)
     *
     * @param pos a position inside the Path
     * @return the Triple contained in the position given as parameter, or null if no triple exists in the given
     * position
     */
    public Triple get(int pos) {
        if (pos > -1 && path != null && pos < path.size()) {
            return path.get(pos);
        } else {
            return null;
        }
    }

    /**
     * Returns the class in a given position (starting at 0)
     *
     * @param pos a position inside the Path
     * @return the class contained in the position given as parameter, or null if no class exists in the given
     */
    public String getClass(int pos) {
        if (pos > -1 && path != null && pos < getClassNumber() - 1) {
            return path.get(pos).getOrigin();
        } else if (pos == getClassNumber() - 1) {
            return path.get(pos - 1).getDestination();
        } else {
            return null;
        }
    }

    /**
     * Removes the Triple in a given position
     *
     * @param pos the position of the Triple to remove (starting at 0)
     */
    public void removeElementAt(int pos) {
        if (pos > -1 && path != null && pos < path.size()) {
            path.remove(pos);
        }
    }

    /**
     * Returns the first Triple of the path
     *
     * @return the first Triple of the path
     */
    public Triple firstElement() {
        if (path.size()>0){
            return path.get(0);
        }
        return null;
    }

    /**
     * Returns the last Triple of the path
     *
     * @return the last Triple of the path
     */
    public Triple lastElement() {
        if (path.size()>0){
            return path.get((path.size()-1));
        }
        return null;
    }

    /**
     * Indicates whether this Path is empty
     *
     * @return true if the Path is empty
     */
    public boolean isEmpty() {
        return path.isEmpty();
    }


    /**
     * Indicates whether a given path subsumes this path. Path A is subsumed to path B (or path B sumsumes path A)
     * if:
     * a) the classes and relations in path A are subclasses/subrelations to the corresponding classes and relations
     * in path B
     *
     * b) the restrictions in path A and path B are equal
     *
     * @param comparePath the Path that can subsume "this" Path
     * @param model       the RDF schema associated
     * @return true if "this" Path is subsumed by comparePath
     */
    public boolean isSubsumed(Path comparePath, OWLBasicModel2 model){
        return isPathSubsumed(comparePath, model);
    }


    /**
     * <pre>
     * check if "this" Path is subsumed by comparePath.
     * <p/>
     * "this" Path is subsumed by compare Path if:
     * <p/>
     *    1. Direct inclusion: "this" is directly included in comparePath.
     *       given comparePath as     ClassA -> Rel -> ClassB -> Rel2 ->ClassC
     *       if "this" is ClassA -> Rel ->ClassB->Rel2->ClassC , than it is considered subsumed
     *       because directly included.
     * <p/>
     *    2. Inheritance: "this" is a path composed (totally or partially) by subclasses of comparePath.
     *       given comparePath as     ClassA -> Rel -> ClassB
     *       and considering the classes ClassB1 and classB2 as subclasses of classB
     *       so if "this" is ClassA -> Rel -> ClassB1 or ClassA -> Rel -> ClassB2
     *       then it is considered subsumed, because composed by subclasses
     * <p/>
     *    3. Variable usage: "this" is a path that contain an instanciation of the variables that occur in
     *      comparePath.
     *      given comparePath as ClassA -> Rel -> ?var1
     *      and considering the classC and classD as classes reachable from classA through
     *      the relation Rel
     *      so if "this" is ClassA -> Rel -> ClassC or ClassA -> Rel -> ClassD
     *      then it is considered subsumed, because it is an instanciation of the
     *      comparePath parameter
     * <p/>
     * </pre>
     *
     * @param comparePath the Path that can subsume "this" Path
     * @param model       the RDF schema associated
     * @return true if "this" Path is subsumed by comparePath
     */
    public boolean isPathSubsumed(Path comparePath, OWLBasicModel2 model)   {
        if (comparePath == null) {
            return false;
        }
        if (comparePath.size() != this.size()) {
            return false;
        }
        List<Triple> correspondentPath = compareRelations(comparePath);
        if (correspondentPath == null) {
            return false;
        }
        if (correspondentPath.size() != this.size()) {
            return false;
        }


        if (correspondentPath.size()>0) {
            Triple thisTriple = path.get(0);
            Triple cpTriple = comparePath.get(0);
            if (thisTriple == null || cpTriple == null || cpTriple.getOrigin() == null || cpTriple.getOrigin().length() < 1) {
                return false;
            }
            if (cpTriple.getOrigin().charAt(0) != '?' && !isSubclass(thisTriple.getOrigin(), cpTriple.getOrigin(), model)) {
                return false;
            }

            for (int i = 0; i < path.size(); i++) {
                thisTriple = path.get(i);
                cpTriple = comparePath.get(i);
                if (thisTriple == null || cpTriple == null || cpTriple.getDestination() == null || cpTriple.getDestination().length() < 1) {
                    return false;
                }
                if (cpTriple.getDestination().charAt(0) != '?' && !isSubclass(thisTriple.getDestination(), cpTriple.getDestination(), model)) {
                    {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Retrieves true if the relations are coincident
     * @param comparedPath path to compare
     * @return  true if the relations are coincident, false otherwise
     */
    private List<Triple> compareRelations(Path comparedPath) {
        if (path.size() < 1) {
            return null;
        }

        String rel = path.get(0).getRelation();
        if (!comparedPath.containsRelation(rel)) {
            return null;
        }
        List<Triple> comp = comparedPath.getPath();
        int first = -1;
        for (int i = 0; i < comp.size(); i++) {
            if (comp.get(i) != null && comp.get(i).getRelation().equals(rel)) {
                first = i;
                i = comp.size();
            }
        }
        if (first < 0) {
            return null;
        }
        int len = first + path.size();

        if (len > comp.size()) {
            return null;
        }
        String rel2;
        List<Triple> res = new ArrayList<Triple>();
        for (int j = 0; j < path.size(); j++) {
            rel2 = comp.get(first + j).getRelation();
            rel = path.get(j).getRelation();
            if (!rel.equals(rel2)) {
                return null;
            }
            res.add(comp.get(first + j));
        }

        return res;
    }

    /**
     * This method retrieves true if the parameter subclass is a subclass of the parameter superclass
     * @param subClass subclass to compare
     * @param superClass superclass to compare
     * @param model owl basic model
     * @return true if the parameter subclass is a subclass of the parameter superclass, false otherwise
     */
    private boolean isSubclass(String subClass, String superClass, OWLBasicModel2 model)   {
        //System.out.println("CHECK IF " + subClass + " is subclass of " + superClass);
        return subClass.equals(superClass) || contains(model.getRDFClassParents(subClass), model, superClass);
    }

    /**
     * This method returns true if the parameter superclass belongs to the list of parents
     * @param parents list of parents
     * @param model owl basic model
     * @param superClass superclass to compare
     * @return true if the parameter superclass belongs to the list of parents, false otherwise
     */
    private boolean contains (List<String> parents, OWLBasicModel2 model, String superClass)   {
        boolean result;
        if (parents.contains(superClass)){
            return true;
        }
        for (String parent: parents){
            result = contains(model.getRDFClassParents(parent), model, superClass);
            if (result){
                return true;
            }
        }
        return false;
    }

    /**
     * check if the class given as parameter exists in the Path
     *
     * @param searchClass the searched class
     * @return true if the class given as parameter exists in the Path
     */
    public boolean containsClass(String searchClass) {

        if (path != null && path.size() > 0) {
            Triple currentTriple = path.get(0);
            if (currentTriple != null && currentTriple.getOrigin().equals(searchClass)) {
                return true;
            }
            for (Triple aPath : path) {
                currentTriple = aPath;
                if (currentTriple != null && currentTriple.getDestination().equals(searchClass)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * check if the relation given as parameter exists in the Path
     *
     * @param searchRelation the searched class
     * @return true if the relation given as parameter exists in the Path
     */
    public boolean containsRelation(String searchRelation) {
        if (path != null && path.size() > 0) {

            for (Triple currentTriple : path) {
                if (currentTriple != null && currentTriple.getRelation().equals(searchRelation)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * compares "this" object with the object passed as parameter
     *
     * @param obj the object to be compared
     * @return true if "this" instance variable is equal to the parameter one
     */
    public boolean equals(Object obj) {
        if (obj instanceof Path) {
            Path p = (Path) obj;
            return this.path.equals(p.getPath());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return path.hashCode();
    }

    public String toString() {
        return path.toString() + " Restricted:" + restrictions;
    }

    private int getPos(Path p, String first) {
        for (int i = 0; i < p.size(); i++) {
            if (p.get(i).getOrigin().equals(first)) {
                return i;
            }
        }
        return (-1);
    }

    /**
     * Retrieves true if is a sub path of super path
     * @param superPath super path
     * @return true if is a sub path of super path, false otherwise
     */
    public boolean isSubPathOf(Path superPath) {
        if (superPath == null) {
            return false;
        }
        if (superPath.size() < this.size()) {
            return false;
        }


        String first = this.get(0).getOrigin();
        if (!superPath.containsClass(first)) {
            return false;
        }

        int pos = getPos(superPath, first);
        if (pos < 0) {
            return false;
        }
        int len = superPath.size() - pos;
        if (len < this.size()) {
            return false;
        }

        for (int i = 0; i < this.size(); i++) {
            boolean rel = this.get(i).getRelation().equals(superPath.get(pos + i).getRelation());
            boolean rang = this.get(i).getDestination().equals(superPath.get(pos + i).getDestination());
            if (!(rel && rang)) {
                return false;
            }
        }


        return true;
    }

    /**
     * compares this path with the object passed as parameter
     *
     * @param obj the object to be compared
     * @return a negative value if this precedes obj, zero if this.equals(obj), a positive value if obj precedes this
     */
    public int compareTo(Object obj) {
        if (obj instanceof Path) {
            Path pt = (Path) obj;
            if (this.size() == pt.size()) {
                return (this.toString().compareTo(pt.toString()));
            } else {
                if (this.size() > pt.size()) {
                    return 1;
                }
                if (this.size() < pt.size()) {
                    return -1;
                }
            }
        }
        return 1;
    }


}
